Cross-platform support, init/install subcommands, setup.sh bootstrap#2
Merged
Conversation
This is a behavior break that gives up macOS Keychain in exchange for
a working Windows / Linux story. Two coupled changes:
1. Credential storage: per-user JSON file at OS-appropriate config dir
macOS / Linux: $XDG_CONFIG_HOME/mcp-medium-reader/credentials.json
(defaulting to ~/.config/...)
Windows: %APPDATA%/mcp-medium-reader/credentials.json
File mode is 0600 on POSIX (atomic write via tmp+rename); Windows
relies on the default per-user ACL under %APPDATA%. The previous
`security` CLI wrappers are gone. New module src/credentials.ts
keeps the same external API (getSecret / setSecret / deleteSecret /
listPresence / readAll) so callers don't change.
Override $MCP_MEDIUM_READER_CONFIG_DIR repurposed for tests and
non-default layouts.
2. Drop "os": ["darwin"] from package.json and remove assertDarwin()
from src/index.ts. Server now runs on Linux, macOS, and Windows.
3. Cross-platform spawn: switch zmedium.ts from node:child_process
spawn to cross-spawn, since Node's CVE-2024-27980 mitigation
refuses to spawn .bat / .cmd files directly and the Ruby gem
installs as a .bat shim on Windows. cross-spawn handles both
POSIX shell scripts and Windows .bat shims with proper arg
quoting (no shell:true, no injection risk).
src/platform.ts probes ZMediumToMarkdown.bat / .cmd / bare on
Windows; bare on POSIX. Version probe uses execFile + shell:true
on Windows (fixed args, no user input).
Other touch-ups:
- errors.ts: KeychainError -> CredentialsError
- warnings.ts: KeychainSnapshot type -> CredentialsSnapshot
- doctor.ts: prints config dir + creds file path instead of "service:
mcp-medium-reader"
- setup.ts: prints credentials file path; final hint points users at
the new `install` / `init` subcommands.
Tests: keychain.test.ts is replaced by credentials.test.ts which
uses MCP_MEDIUM_READER_CONFIG_DIR + a fresh tmp dir per test, asserts
the file lives at credentials.json under the config dir, and verifies
0600 perms on POSIX.
…allback
Three hand-written CLI wrappers, one per native secret store, plus a
plain-file fallback. The dispatcher in src/credentials.ts picks one
based on `process.platform` (or env override) and caches it.
src/credentials/types.ts
Backend interface + Account types + SERVICE_NAME constant.
src/credentials/keychain.ts (macOS)
`security` CLI wrapper. find/add/delete-generic-password with
-s mcp-medium-reader. Exit-44 -> null/no-op. ENOENT carries a
setup-friendly error pointing at MCP_MEDIUM_READER_BACKEND=file.
src/credentials/secrettool.ts (Linux)
`secret-tool` CLI (libsecret) wrapper. lookup / store / clear,
keyed on `service mcp-medium-reader account <name>`. `store`
reads the value from stdin so it never appears on argv. probe()
via `secret-tool --version` so the dispatcher can fall back to
the file backend on headless boxes without libsecret + DBus.
src/credentials/dpapi.ts (Windows)
powershell.exe + DPAPI (the same per-user encryption that backs
Credential Manager / Chrome's saved passwords). Each value is
encrypted via ConvertTo/ConvertFrom-SecureString and stored as a
base64 blob inside %APPDATA%\\mcp-medium-reader\\credentials.json.
PowerShell scripts passed via -EncodedCommand (UTF-16LE base64)
so argv quoting is irrelevant; values are piped over stdin.
listPresence skips decryption — empty-string check on the file
is enough to answer "is it set?".
src/credentials/file.ts
Plain JSON fallback (atomic tmp+rename + chmod 0600 on POSIX,
default ACL on Windows). Honors MCP_MEDIUM_READER_CONFIG_DIR.
src/credentials.ts (dispatcher)
Picks backend at first use:
MCP_MEDIUM_READER_BACKEND=keychain|secret-tool|dpapi|file
else: darwin -> keychain, win32 -> dpapi,
linux -> secret-tool (probed; falls through to file with
an actionable stderr notice if libsecret is missing),
other -> file
Re-exports public API (getSecret/setSecret/deleteSecret/readAll/
listPresence) plus getBackendName / getCredentialsLocation for
doctor / setup display.
src/setup.ts, src/doctor.ts
Display the resolved backend name and storage location instead
of the previous hard-coded "service: mcp-medium-reader" string.
test/credentials.test.ts
Forces backend=file via env so the suite is platform-agnostic;
keeps the existing get/set/delete/listPresence/readAll cases
plus new "backend selection honors override / matches platform
default / tolerates unknown value" cases.
Two new subcommands close the gap between "npm install -g" and a
working MCP client.
src/install.ts
Knows the four supported MCP clients and where their configs
live on each OS:
claude-desktop -> ~/Library/.../Claude/claude_desktop_config.json
/ %APPDATA%/Claude/... / ~/.config/Claude/...
claude-code -> ~/.claude.json
codex -> ~/.codex/config.toml (TOML via smol-toml)
gemini -> ~/.gemini/settings.json
installToClient loads the existing config (creating an empty one
if missing), sets <jsonPath> to {command:"mcp-medium-reader"},
and writes back. Reports added / updated / unchanged / error per
target. runInstall(--clients=...) lets users opt into a subset.
SERVER_KEY = "medium-reader" is the registered name shown in the
LLM tool-call UI.
src/init.ts
One-shot orchestrator: gem version check -> credentials backend
probe -> runSetup() -> runInstall(). Mirrors what a packaged
installer would do.
src/cli.ts
Adds `init` and `install` subcommands; updates --help.
test/install.test.ts
Covers added / updated / unchanged outcomes for both JSON and
TOML formats; preservation of existing keys; malformed JSON
surfaces as a non-fatal error result; defaultTargets shape is
stable.
.github/workflows/ci.yml
Now ubuntu/macos/windows x Node 20/22 matrix. The "os": ["darwin"]
constraint was already removed from package.json; with cross-spawn
handling Windows .bat shims and the credential backend dispatcher
selecting per-platform stores, the test suite (forced to the file
backend via env) runs identically on all three OSes.
README rewrites the framing from "macOS-only" to "cross-platform" and
documents:
* the per-platform credentials backend table (Keychain / secret-tool
/ DPAPI / file) and the MCP_MEDIUM_READER_BACKEND override
* `mcp-medium-reader init` as the recommended onboarding flow (deps +
credentials + MCP client install in one shot)
* `mcp-medium-reader install --clients=...` and the four supported
client config paths per OS
* SERVER_KEY = "medium-reader" so users know what name appears in
their LLM tool-call UI
* troubleshooting rows for libsecret missing on Linux and powershell
missing on Windows (both point at the file fallback)
CHANGELOG follows Keep-a-Changelog: Added section enumerates the
backends, install/init subcommands, four-client default install, and
the multi-OS CI matrix; Changed section flags the removal of the
darwin-only constraint and the keychain.ts -> credentials.ts rename.
Single-command onboarding for macOS / Linux / Git Bash / WSL:
bash <(curl -fsSL .../setup.sh)
The script:
1. Detects platform (Darwin / Linux / MINGW|MSYS|CYGWIN).
2. Probes Ruby >= 3.2 and Node.js >= 18 with platform-aware install
hints (brew / apt / dnf / RubyInstaller / nodejs.org).
3. Installs (or upgrades) the ZMediumToMarkdown gem.
4. Installs (or upgrades) mcp-medium-reader globally via npm.
5. Verifies both binaries actually land on PATH and surfaces a
clear hint if not (Homebrew Ruby PATH gotcha, npm prefix on
Windows, etc.).
6. exec's `mcp-medium-reader init` for the interactive credentials
+ 4-client MCP install pass.
UX details:
- --no-init / --no-gem / --no-npm flags for partial runs.
- Color output gated on `[ -t 1 ]` (no escape codes when piped).
- When stdin isn't a TTY (curl-pipe-bash), skips the interactive
init and prints "open a terminal and run mcp-medium-reader init"
instead of hanging on a readline prompt that can't receive input.
- `set -euo pipefail` for strict failure semantics.
- --help dumps the leading comment block.
README rewrites the install section to lead with the curl-piped
setup.sh + a Windows-native fallback path.
…tform
Two CI failures, fixed together.
1. `npm ci` failed in 6-15 seconds across all 12 jobs because no
`package-lock.json` is committed. Switch to `npm install
--no-audit --no-fund`. The semver ranges in package.json are tight
enough for reproducible builds; lockfile can be added later in a
separate PR after a clean local install.
2. test/fsutil.test.ts asserted `resolveOutputDir('/var/foo') ===
'/var/foo'`. That's true on POSIX but on Windows path.resolve
prepends the current drive (`C:\var\foo`). Compare against
`path.resolve('/var/foo')` on both sides so the assertion shape
is identical on every platform.
Verified locally: `npm install && npm run build && npm test` →
56 tests pass on Node 22 / Ubuntu.
src/keychain.ts is left over from the initial PR #1's macOS-only implementation. Its only export was the `security` CLI wrapper which now lives at src/credentials/keychain.ts (one of four backends behind the src/credentials.ts dispatcher). It also imports `KeychainError` which was renamed to `CredentialsError` in errors.ts during the cross-platform refactor — so the file actually fails `tsc` on its own. Removing it unbreaks the build.
This test was tied to the old src/keychain.ts module which is gone in the cross-platform refactor. It imports `KeychainError` and `SERVICE` from a path / symbols that no longer exist, so vitest can't even collect the file — that's the symptom CI was hitting. Coverage for the security CLI wrapper now lives indirectly in test/credentials.test.ts (which forces backend=file via env so it runs the same on all three OSes). The macOS-only security argv shape still lives in src/credentials/keychain.ts; we deliberately don't unit-test it because mocking node:child_process.execFile portably is brittle and the real wrapper is exercised end-to-end via `mcp-medium-reader doctor` on macOS.
actions/setup-node@v4's `cache: 'npm'` option fails before npm runs when it can't find package-lock.json / npm-shrinkwrap.json / yarn.lock to compute the cache key. We don't ship a lockfile yet, so the cache config errored out every job in setup before reaching the install step. Remove the `cache:` line; revisit when we commit a lockfile.
zhgchgli0718
added a commit
that referenced
this pull request
May 5, 2026
When system Ruby is below the gem's required 3.2, defer to rbenv if it's
available rather than just bailing:
1. command -v rbenv -> if missing, fall back to the original "upgrade
Ruby and re-run" hint with a pointer to rbenv.
2. Probe rbenv versions --bare for an already-installed >= 3.2; if
found, switch via RBENV_VERSION and prepend $(rbenv root)/shims to
PATH so subsequent `ruby`, `gem`, `ZMediumToMarkdown` calls hit it.
3. If nothing >= 3.2 is installed, pick the latest stable 3.x from
rbenv install --list and rbenv install it (TTY confirmation prompt;
curl-pipe-bash skips the prompt and proceeds).
4. After install, re-validate by reading RUBY_VERSION from the now-
active rbenv ruby; abort if still below 3.2.
Adds --no-rbenv flag for users who'd rather fail fast than have the
script touch their Ruby toolchain. The gem-install step now also runs
`rbenv rehash` so the freshly installed ZMediumToMarkdown shim shows
up immediately on PATH without a shell restart.
(Pushed direct to main: rbenv changes landed on the working branch
after PR #2 was already merged, so they missed the merge.)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to the initial merge (#1). Drops the macOS-only restriction and lands one-shot onboarding.
What's new since #1
Cross-platform support (macOS / Linux / Windows)
Each platform uses its native secret store via a hand-written CLI wrapper.
src/credentials.tsdispatches based onprocess.platform; override withMCP_MEDIUM_READER_BACKEND={keychain|secret-tool|dpapi|file}.securityCLIsecret-toolCLIpowershell.exe0600on POSIX,%APPDATA%per-user ACL on Windows)On Linux the dispatcher probes for
secret-tooland falls back to the file backend with a stderr notice if libsecret-tools isn't installed.cross-spawnfor proper Windows.batshim handling around Node's CVE-2024-27980 mitigation.src/platform.tsprobesZMediumToMarkdown.bat/.cmd/ bare on Windows, bare on POSIX.package.jsonno longer declares"os": ["darwin"].assertDarwin()removed from server entry.Auto-setup:
mcp-medium-reader initOne-shot orchestrator: gem version check → credentials walkthrough → install for all four supported MCP clients. Each step is also exposed as its own subcommand (
setup,install,doctor).~/Library/Application Support/Claude/claude_desktop_config.json· Win:%APPDATA%\Claude\claude_desktop_config.json· Linux:~/.config/Claude/claude_desktop_config.json~/.claude.json~/.codex/config.toml(TOML viasmol-toml)~/.gemini/settings.jsonLimit clients with
--clients=claude-desktop,gemini. Existing config keys are preserved; reportsadded/updated/unchanged/errorper target.setup.sh— single-command bootstrapbash <(curl -fsSL https://raw.githubusercontent.com/ZhgChgLi/mcp-medium-reader/main/setup.sh)Probes Ruby ≥ 3.2 and Node.js ≥ 18 with platform-aware install hints (brew / apt / dnf / RubyInstaller / nodejs.org), installs the gem and the npm package, verifies both binaries land on PATH, then
execsmcp-medium-reader init. Flags:--no-init/--no-gem/--no-npm/--help. Detects curl-pipe-bash (no TTY) and skips interactiveinitwith a manual-run hint instead of hanging on a readline prompt.Commits
a96ee2aCross-platform credential storage; drop macOS-only restrictionf8e4abcThree native-backend wrappers (security / secret-tool / DPAPI) + file fallback + dispatcher6be0abeinstall/initsubcommands; multi-OS CI matrix (ubuntu × macos × windows × Node 20 / 22)a9aed85README and CHANGELOG rewrite for cross-platform + new subcommands1469383setup.shone-shot bootstrapTest plan
test/credentials.test.ts— file backend (forced via env): get/set/delete/listPresence/readAll,0600perms on POSIX. Backend selection: env override, platform default, unknown value falls through.test/install.test.ts— JSON/TOML config writes; existing-key preservation; added/updated/unchanged/error outcomes;defaultTargetsshape stable..github/workflows/ci.ymlrunsnpm ci && npm run build && npm teston ubuntu-latest / macos-latest / windows-latest × Node 20 & 22.bash setup.shend-to-end on macOS / Linux / Git Bash / WSL.mcp-medium-reader doctorreports the right backend per OS (macOS keychain / Linux secret-tool / Windows dpapi).fileand prints the stderr notice.gem install ZMediumToMarkdown && npm install -g mcp-medium-reader && mcp-medium-reader init.install --clients=claude-desktoponly touches Claude Desktop config.Setup guide: https://github.com/ZhgChgLi/ZMediumToMarkdown/wiki/Setting-Up-Medium-Cookies-and-a-Cloudflare-Worker-Proxy
Generated by Claude Code